/*
 * Copyright (c) 2011, Yawning <yawninglol at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   * Neither the name of the Mew Developers nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package yawning.mew.protwarrior;

import java.util.HashMap;

import yawning.mew.MewProperty;
import yawning.mew.ProcEffect;
import yawning.mew.Report;
import yawning.mew.BuffsDebuffs.Buff;
import yawning.mew.Report.Stat;
import yawning.mew.protwarrior.ProtWarriorReport.ProtWarriorStat;
import yawning.mew.protwarrior.ProtWarriorSimulator.ProtWarriorTargetData;
import yawning.mew.sim.Simulation;
import yawning.mew.sim.SimulationEngine;
import yawning.mew.sim.SimulationProc;
import yawning.mew.sim.SimulationTarget;
import yawning.mew.sim.Strategy.IAction;
import yawning.mew.sim.Strategy.IStatus;

public class ProtWarriorSimulation implements Simulation {
	private static final String DEFAULT_STRATEGY_FILE = "yawning/mew/protwarrior/ProtWarriorSimulator.script";
	private static final String VERSION_INFO = "x.x.x";

	public enum Action implements IAction {
		NONE,

		DEMORALIZINGSHOUT,
		PUMMEL,

		BATTLESHOUT,
		COMMANDINGSHOUT,
		BERSERKERRAGE,
		CHARGE,
		INNERRAGE,

		SHIELDSLAM,
		THUNDERCLAP,
		CONCUSSIONBLOW,
		REND,
		REVENGE,
		DEVASTATE,
		SHOCKWAVE,

		HEROICTHROW,
		VICTORYRUSH,

		HEROICSTRIKE,
		CLEAVE,

		RECKLESSNESS,
		RETALIATION,
		SHIELDWALL,
		SHIELDBLOCK,
		LASTSTAND,
		ENRAGEDREGENERATION,

		POTION,
		HEROISM,
		UNHOLYFRENZY,
		SHATTERINGTHROW,

		// Swing timer control.
		AUTOATTACK_START,
		AUTOATTACK_STOP
	}

	public static class SimulatorProperty {
		public static final MewProperty ASSUMEEXTERNALDEMO = new MewProperty("Simulator.AssumeExternalDemo", false);		// Mob is always fully debuffed with the Physcal Daamge reduction debuff.
		public static final MewProperty ASSUMEEXTERNALFF = new MewProperty("Simulator.AssumeExternalFF", false);			// Mob is always fully debuffed with the Major Armor debuff.
		public static final MewProperty ASSUMEEXTERNALTC = new MewProperty("Simulator.AssumeExternalTC", false);			// Mob is always fully debuffed with the Attack Speed reduction debuff.
		public static final MewProperty UNLIMITEDRAGE = new MewProperty("Simulator.UnlimitedRage", false);				// Infinite rage.
		public static final MewProperty VENGANCE = new MewProperty("Simulator.Vengeance", false);						// Vengeance support.
		public static final MewProperty REPORTMIT = new MewProperty("Simulator.ReportMitigation", false);				// Report mitigation.
	}

	@Override
	public IStatus initSimulation(SimulationEngine su) {
		ProtWarriorSimulator state = new ProtWarriorSimulator(su);
		if (!su.hasStrategy()) su.setStrategy(Thread.currentThread().getContextClassLoader().getResourceAsStream(DEFAULT_STRATEGY_FILE));
		return state;
	}

	@Override
	public Report getBaseReport() {
		return new ProtWarriorReport();
	}

	@Override
	public Report getReport(IStatus status) {
		if (!(status instanceof ProtWarriorSimulator)) return null;

		ProtWarriorSimulator state = (ProtWarriorSimulator) status;
		ProtWarriorReport report = new ProtWarriorReport();
		ProtWarriorTargetData primaryTargetData = (ProtWarriorTargetData) state.mTargetList.get(0).getTargetData();

		double encounterDuration = state.mSimEngine.getEndTime();

		double totalTPS = 0.0;

		report.setResult(Stat.DAMAGE_DEALT_PER_SECOND, 0);

		// Survivability
		// XXX: Does Blood Craze cause threat?  Assume yes for now.
		double bloodCrazeTPS = state.mBloodCrazeEv.mBloodCrazeHeal * 0.5 * state.mModel.multThreatStance / encounterDuration;
		double enragedRegenerationTPS = state.mEnragedRegenerationAction.mEnragedRegenerationHeal * 0.5 * state.mModel.multThreatStance / encounterDuration;
		double demoralizingShoutTPS = (state.mDemoralizingShoutAction.mNrDemoHit * state.mModel.mLevelDep.getDemoralizingShoutThreat() * state.mModel.multThreatStance) / encounterDuration;
		report.setResult(ProtWarriorStat.DEMORALIZING_SHOUT_UPTIME, state.mDemoralizingShoutAction.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.SHIELD_BLOCK_UPTIME, state.mShieldBlockAction.getShieldBlockUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.TIER_12_4PC_UPTIME, state.mShieldBlockAction.get4T12UpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.SHIELD_WALL_UPTIME, state.mShieldWallAction.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.ENRAGED_REGEN_UPTIME, (double) state.mEnragedRegenerationAction.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.ENRAGED_REGEN_AVG_SZ, (state.mEnragedRegenerationAction.mNrHeals > 0) ? (double) state.mEnragedRegenerationAction.mEnragedRegenerationHeal / state.mEnragedRegenerationAction.mNrHeals : 0);
		report.setResult(ProtWarriorStat.ENRAGED_REGEN_HPS, (double) state.mEnragedRegenerationAction.mEnragedRegenerationHeal / encounterDuration);
		report.setResult(ProtWarriorStat.BLOOD_CRAZE_UPTIME, (double) state.mBloodCrazeEv.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.BLOOD_CRAZE_AVG_SZ, (state.mBloodCrazeEv.mNrHeals > 0) ? (double) state.mBloodCrazeEv.mBloodCrazeHeal / state.mBloodCrazeEv.mNrHeals : 0);
		report.setResult(ProtWarriorStat.BLOOD_CRAZE_HPS, (double) state.mBloodCrazeEv.mBloodCrazeHeal / encounterDuration);
		report.setResult(ProtWarriorStat.VICTORY_RUSH_HPS, (double) state.mVictoryRushAction.mVictoryRushHeal / encounterDuration);
		report.setResult(ProtWarriorStat.LAST_STAND_UPTIME, state.mLastStandAction.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.HOLD_THE_LINE_UPTIME, state.mHoldTheLineEv.getUpTime() / encounterDuration * 100);

		// Tier 13 2pc (Survivability)
		if (state.mTier_13_2pcEv.mIsEnabled) {
			if (state.mTier_13_2pcEv.mNrPossibleAbsorbs > 0) {
				// Display how many mob swings the absorb was actually up for since that's a more useful metric.
				report.setResult(ProtWarriorStat.TIER_13_2PC_UPTIME, (double) state.mTier_13_2pcEv.mNrAbsorbs / state.mTier_13_2pcEv.mNrPossibleAbsorbs * 100);	// XXX: NAN
			} else {
				// Either mob damage is totally disabled or the warrior got really really lucky.  Switch to the buff uptime.
				// Note: This will skew statistics slightly if it's the one in a few billion chance that you dodged everything.
				report.setResult(ProtWarriorStat.TIER_13_2PC_UPTIME, (double) state.mTier_13_2pcEv.getUpTime() / encounterDuration * 100);
			}
			report.setResult(ProtWarriorStat.TIER_13_2PC_PROCS, state.mTier_13_2pcEv.mNrProcs);
			report.setResult(ProtWarriorStat.TIER_13_2PC_ABSORBED, state.mTier_13_2pcEv.mDamageAbsorbed);
			if (state.mTier_13_2pcEv.mNrProcs > 0) {
				report.setResult(ProtWarriorStat.TIER_13_2PC_AVG_SZ, (double) state.mTier_13_2pcEv.mTotalAbsorb / state.mTier_13_2pcEv.mNrProcs);
				report.setResult(ProtWarriorStat.TIER_13_2PC_PROC_INTERVAL, encounterDuration / state.mTier_13_2pcEv.mNrProcs);
			} else {
				// Uh, 2T13 did not proc at all.  Failwarrior. :(
				report.setResult(ProtWarriorStat.TIER_13_2PC_AVG_SZ, 0);
				report.setResult(ProtWarriorStat.TIER_13_2PC_PROC_INTERVAL, 0);
			}
		} else {
			report.setResult(ProtWarriorStat.TIER_13_2PC_ABSORBED, 0);
			report.setResult(ProtWarriorStat.TIER_13_2PC_AVG_SZ, 0);
			report.setResult(ProtWarriorStat.TIER_13_2PC_PROC_INTERVAL, 0);
			report.setResult(ProtWarriorStat.TIER_13_2PC_PROCS, 0);
			report.setResult(ProtWarriorStat.TIER_13_2PC_UPTIME, 0);
		}

		// Mob White Attacks
		double mobWhiteAttackDTPS = state.mMobWhiteDamageTaken / encounterDuration;
		long mobWhiteAttacks = state.mNrMobWhiteMiss + state.mNrMobWhiteDodge + state.mNrMobWhiteParry + state.mNrMobWhiteCrit + state.mNrMobWhiteCritBlock + state.mNrMobWhiteBlock + state.mNrMobWhiteHit;
		report.setResult(ProtWarriorStat.MOB_WHITE_ATTACK_DTPS, mobWhiteAttackDTPS);
		report.setResult(ProtWarriorStat.MOB_WHITE_ATTACKS, mobWhiteAttacks);
		if (mobWhiteAttacks > 0) {
			report.setResult(ProtWarriorStat.P_MOB_WHITE_MISS, (double) state.mNrMobWhiteMiss / mobWhiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_WHITE_DODGE, (double) state.mNrMobWhiteDodge / mobWhiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_WHITE_PARRY, (double) state.mNrMobWhiteParry / mobWhiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_WHITE_CRIT_BLOCK, (double) state.mNrMobWhiteCritBlock / mobWhiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_WHITE_BLOCK, (double) state.mNrMobWhiteBlock / mobWhiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_WHITE_CRIT, (double) state.mNrMobWhiteCrit / mobWhiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_WHITE_HIT, (double) state.mNrMobWhiteHit / mobWhiteAttacks * 100);
			report.setResult(ProtWarriorStat.MOB_WHITE_INTERVAL, encounterDuration / mobWhiteAttacks);
			report.setResult(ProtWarriorStat.MOB_WHITE_AVG_DMG, state.mMobWhiteDamageTaken / mobWhiteAttacks);
		} else {
			// This should never happen unless mob damage is disabled somehow, so just suppress the output.
			report.mHasValidMobWhiteDamage = false;
		}

		// Mob Yellow Attacks
		double mobYellowAttackDTPS = state.mMobYellowDamageTaken / encounterDuration;
		long mobYellowAttacks = state.mNrMobYellowMiss + state.mNrMobYellowDodge + state.mNrMobYellowParry + state.mNrMobYellowCritBlock + state.mNrMobYellowBlock + state.mNrMobYellowHit;
		report.setResult(ProtWarriorStat.MOB_YELLOW_ATTACK_DTPS, mobYellowAttackDTPS);
		report.setResult(ProtWarriorStat.MOB_YELLOW_ATTACKS, mobYellowAttacks);
		if (mobYellowAttacks > 0) {
			report.setResult(ProtWarriorStat.P_MOB_YELLOW_MISS, (double) state.mNrMobYellowMiss / mobYellowAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_YELLOW_DODGE, (double) state.mNrMobYellowDodge / mobYellowAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_YELLOW_PARRY, (double) state.mNrMobYellowParry / mobWhiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_YELLOW_CRIT_BLOCK, (double) state.mNrMobYellowCritBlock / mobYellowAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_YELLOW_BLOCK, (double) state.mNrMobYellowBlock / mobYellowAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_YELLOW_HIT, (double) state.mNrMobYellowHit / mobYellowAttacks * 100);
			report.setResult(ProtWarriorStat.MOB_YELLOW_INTERVAL, encounterDuration / mobYellowAttacks);
			report.setResult(ProtWarriorStat.MOB_YELLOW_AVG_DMG, state.mMobYellowDamageTaken / mobYellowAttacks);
		} else {
			// This should never happen unless mob damage is disabled somehow, so just suppress the output.
			report.mHasValidMobYellowDamage = false;
		}

		// Mob Magic Attacks
		double mobMagicAttackDTPS = state.mMobMagicDamageTaken / encounterDuration;
		long mobMagicAttacks = state.mNrMobMagicMiss + state.mNrMobMagicHit;
		report.setResult(ProtWarriorStat.MOB_MAGIC_ATTACK_DTPS, mobMagicAttackDTPS);
		report.setResult(ProtWarriorStat.MOB_MAGIC_ATTACKS, mobMagicAttacks);
		if (mobMagicAttacks > 0) {
			report.setResult(ProtWarriorStat.P_MOB_MAGIC_MISS, (double) state.mNrMobMagicMiss / mobMagicAttacks * 100);
			report.setResult(ProtWarriorStat.P_MOB_MAGIC_HIT, (double) state.mNrMobMagicHit / mobMagicAttacks * 100);
			report.setResult(ProtWarriorStat.MOB_MAGIC_INTERVAL, encounterDuration / mobMagicAttacks);
			report.setResult(ProtWarriorStat.MOB_MAGIC_AVG_DMG, state.mMobMagicDamageTaken / mobMagicAttacks);
		} else {
			// This should never happen unless mob damage is disabled somehow, so just suppress the output.
			report.mHasValidMobMagicDamage = false;
		}

		// Pummel/Gag Order
		report.setResult(ProtWarriorStat.NR_TARGET_CASTS, primaryTargetData.getSimulationTarget().getTargetCastEv().getNrCasts() + primaryTargetData.getSimulationTarget().getTargetCastEv().getNrInterruptedCasts());
		report.setResult(ProtWarriorStat.NR_INTERRUPTED_CASTS, primaryTargetData.getSimulationTarget().getTargetCastEv().getNrInterruptedCasts());
		report.setResult(ProtWarriorStat.NR_PUMMELS, state.mPummelAction.mNrPummelHit);
		report.setResult(ProtWarriorStat.NR_GAG_ORDERS, state.mHeroicThrowAction.mNrHeroicThrowInterrupts);

		// Threat boosts
		// XXX: Shattering Throw/Sunder Armor uptimes are primary target only.
		report.setResult(ProtWarriorStat.VENGEANCE_CAP, state.mModel.getVengeanceCap());
		report.setResult(ProtWarriorStat.VENGEANCE_AVG, state.mVengeanceEv.getAverageVengeance());
		report.setResult(ProtWarriorStat.SUNDER_ARMOR_UPTIME, primaryTargetData.mDevastateAction.getUpTime() / encounterDuration * 100);
		double bastionOfDefenseTPS = (state.mBastionOfDefenseEv.mNrProcs * state.mModel.mLevelDep.getBastionOfDefenseThreat()) / encounterDuration;
		report.setResult(ProtWarriorStat.BASTION_OF_DEFENSE_UPTIME, state.mBastionOfDefenseEv.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.INNER_RAGE_UPTIME, state.mInnerRageAction.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.RECKLESSNESS_UPTIME, state.mRecklessnessAction.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.RETALIATION_UPTIME, state.mRetaliationAction.getUpTime() / encounterDuration * 100);
		double rudeInterruptionTPS = (state.mRudeInterruptionEv.mNrProcs * state.mModel.mLevelDep.getRudeInterruptionThreat()) / encounterDuration;
		report.setResult(ProtWarriorStat.RUDE_INTERRUPTION_UPTIME, state.mRudeInterruptionEv.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.TIER_12_DPS_2PC_UPTIME, state.mTier_12_Dps_2pcEv.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.SHATTERING_THROW_UPTIME, primaryTargetData.mShatteringThrowEv.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.HEROISM_UPTIME, state.mHeroismEv.getUpTime() / encounterDuration * 100);
		report.setResult(ProtWarriorStat.UNHOLY_FRENZY_UPTIME, state.mUnholyFrenzyEv.getUpTime() / encounterDuration * 100);
		if (state.mModel.mBuffsDebuffs.isBuff(Buff.TRICKSOFTHETRADE)) {
			// Sigh.  The TOTT event returns 100% for getUpTime, so fake this.  It's correct anyway.
			report.setResult(ProtWarriorStat.TOTT_UPTIME, state.mModel.GetCooldownUptime(state.mModel.tottBuffDuration, state.mModel.tottCDDuration, (int) encounterDuration) / encounterDuration * 100);
		} else report.setResult(ProtWarriorStat.TOTT_UPTIME, 0);

		// Rage
		// XXX: Does rage gain from damage generate threat?  Assuming no for now.
		report.setResult(ProtWarriorStat.RAGE_SPENT, state.mRageSpent);
		report.setResult(ProtWarriorStat.RAGE_OVERFLOW, Math.round(state.mRageOverflow));
		if (state.mSimEngine.getConfig().getBool(SimulatorProperty.UNLIMITEDRAGE)) {
			report.mHasValidRageOverflow = false;
		}
		report.setResult(ProtWarriorStat.RAGE_GAINED_AUTO_ATTACK, state.mMeleeEv.mRageGained);
		report.setResult(ProtWarriorStat.RAGE_GAINED_BERSERKER_RAGE, state.mBerserkerRageAction.mRageGained);
		report.setResult(ProtWarriorStat.RAGE_GAINED_CHARGE, state.mChargeAction.mRageGained);
		report.setResult(ProtWarriorStat.RAGE_GAINED_SHOUT, state.mCommandingShoutAction.mRageGained + state.mBattleShoutAction.mRageGained);
		report.setResult(ProtWarriorStat.RAGE_GAINED_SHIELD_SPECIALIZATION, state.mShieldSpecializationRageGained);
		report.setResult(ProtWarriorStat.RAGE_GAINED_MOB_WHITE, Math.round(state.mMobMeleeRageGained));
		report.setResult(ProtWarriorStat.RAGE_GAINED_MOB_YELLOW, Math.round(state.mMobYellowRageGained));
		report.setResult(ProtWarriorStat.RAGE_GAINED_MOB_MAGIC, Math.round(state.mMobMagicRageGained));
		double rageGained = state.mMeleeEv.mRageGained + state.mBerserkerRageAction.mRageGained + state.mChargeAction.mRageGained + state.mCommandingShoutAction.mRageGained + state.mBattleShoutAction.mRageGained + state.mShieldSpecializationRageGained + Math.round(state.mMobMeleeRageGained) + Math.round(state.mMobYellowRageGained) + Math.round(state.mMobMagicRageGained);
		report.setResult(ProtWarriorStat.RAGE_GAINED, rageGained);
		report.setResult(ProtWarriorStat.BATTLE_TRANCE_PROCS, state.mBattleTranceEv.mNrBattleTranceProcs);
		report.setResult(ProtWarriorStat.BATTLE_TRANCE_AVG_RAGE_RETURN, (state.mBattleTranceEv.mNrBattleTranceProcs > 0) ? (double) state.mBattleTranceEv.mBattleTranceRageReturn / state.mBattleTranceEv.mNrBattleTranceProcs : 0);
		double rageGainTPS = (state.mRageGainedCountThreat * state.mModel.mLevelDep.getRageGainThreat()) / encounterDuration; // + battleTraceTPS;
		report.setResult(ProtWarriorStat.RAGE_GAIN_TPS, rageGainTPS);
		double shoutTPS = ((state.mBattleShoutAction.mNrBattleShout * state.mModel.mLevelDep.getBattleShoutThreat() + state.mCommandingShoutAction.mNrCommandingShout * state.mModel.mLevelDep.getCommandingShoutThreat())* state.mModel.multThreatStance) / encounterDuration;
		totalTPS += rageGainTPS;

		// White attack
		report.setResult(ProtWarriorStat.WHITE_ATTACK_DPS, state.mMeleeEv.mWhiteDamage / encounterDuration);
		long whiteAttacks = state.mMeleeEv.mNrWhiteMiss + state.mMeleeEv.mNrWhiteDodge + state.mMeleeEv.mNrWhiteParry + state.mMeleeEv.mNrWhiteGlance + state.mMeleeEv.mNrWhiteCrit + state.mMeleeEv.mNrWhiteHit;
		report.setResult(ProtWarriorStat.WHITE_ATTACKS, whiteAttacks);
		double whiteAttackTPS = 0;
		if (whiteAttacks > 0) {
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_MISS, (double) state.mMeleeEv.mNrWhiteMiss / whiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_DODGE, (double) state.mMeleeEv.mNrWhiteDodge / whiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_PARRY, (double) state.mMeleeEv.mNrWhiteParry / whiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_GLANCE, (double) state.mMeleeEv.mNrWhiteGlance / whiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_CRIT, (double) state.mMeleeEv.mNrWhiteCrit / whiteAttacks * 100);
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_HIT, (double) state.mMeleeEv.mNrWhiteHit / whiteAttacks * 100);
			report.setResult(ProtWarriorStat.WHITE_ATTACK_INTERVAL, encounterDuration / whiteAttacks);
			report.setResult(ProtWarriorStat.WHITE_ATTACK_AVG_DMG, state.mMeleeEv.mWhiteDamage / whiteAttacks);
			whiteAttackTPS = (state.mMeleeEv.mWhiteDamage * state.mModel.multThreatStance) / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_MISS, 0);
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_DODGE, 0);
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_PARRY, 0);
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_GLANCE, 0);
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_CRIT, 0);
			report.setResult(ProtWarriorStat.P_WHITE_ATTACK_HIT, 0);
			report.setResult(ProtWarriorStat.WHITE_ATTACK_INTERVAL, 0);
			report.setResult(ProtWarriorStat.WHITE_ATTACK_AVG_DMG, 0);
		}
		report.setResult(ProtWarriorStat.WHITE_ATTACK_TPS, whiteAttackTPS);
		totalTPS += whiteAttackTPS;

		// Deep Wounds
		report.setResult(ProtWarriorStat.DEEP_WOUNDS_DPS, (double) state.mDeepWoundsEv.mDeepWoundsDamage / encounterDuration);
		int nrDeepWoundsTicks = state.mDeepWoundsEv.mNrDeepWoundsTicks;
		report.setResult(ProtWarriorStat.DEEP_WOUNDS_TICKS, nrDeepWoundsTicks);
		double deepWoundsTPS = 0;
		if (nrDeepWoundsTicks > 0) {
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_AVG_DMG, (double) state.mDeepWoundsEv.mDeepWoundsDamage / nrDeepWoundsTicks);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_ROLLED, state.mDeepWoundsEv.mNrDeepWoundsRolled);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_ROLLED_DMG, state.mDeepWoundsEv.mDeepWoundsDmgRolled);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_MUNCHED, state.mDeepWoundsEv.mNrDeepWoundsMunch);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_MUNCHED_DMG, state.mDeepWoundsEv.mDeepWoundsDmgLostMunch);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_REFRESH_LOST, state.mDeepWoundsEv.mNrDeepWoundsRefreshLost);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_REFRESH_LOST_DMG, state.mDeepWoundsEv.mDeepWoundsDmgLostRefresh);
			double deepWoundsAvgThreat = (state.mDeepWoundsEv.mDeepWoundsDamage * state.mModel.multThreatStance) / nrDeepWoundsTicks;
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_AVG_THREAT, deepWoundsAvgThreat);
			deepWoundsTPS = (state.mDeepWoundsEv.mDeepWoundsDamage * state.mModel.multThreatStance) / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_ROLLED, 0);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_ROLLED_DMG, 0);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_MUNCHED, 0);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_MUNCHED_DMG, 0);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_REFRESH_LOST, 0);
			report.setResult(ProtWarriorStat.DEEP_WOUNDS_REFRESH_LOST_DMG, 0);
		}
		report.setResult(ProtWarriorStat.DEEP_WOUNDS_TPS, deepWoundsTPS);
		totalTPS += deepWoundsTPS;

		// Shield Slam
		report.setResult(ProtWarriorStat.SHIELD_SLAM_DPS, state.mShieldSlamAction.mShieldSlamDamage / encounterDuration);
		long shieldSlams = state.mShieldSlamAction.mNrShieldSlamMiss + state.mShieldSlamAction.mNrShieldSlamDodge + state.mShieldSlamAction.mNrShieldSlamParry + state.mShieldSlamAction.mNrShieldSlamCrit + state.mShieldSlamAction.mNrShieldSlamHit;
		report.setResult(ProtWarriorStat.SHIELD_SLAMS, shieldSlams);
		double shieldSlamTPS = 0;
		if (shieldSlams > 0) {
			report.setResult(ProtWarriorStat.P_SHIELD_SLAM_MISS, (double) state.mShieldSlamAction.mNrShieldSlamMiss / shieldSlams * 100);
			report.setResult(ProtWarriorStat.P_SHIELD_SLAM_DODGE, (double) state.mShieldSlamAction.mNrShieldSlamDodge / shieldSlams * 100);
			report.setResult(ProtWarriorStat.P_SHIELD_SLAM_PARRY, (double) state.mShieldSlamAction.mNrShieldSlamParry / shieldSlams * 100);
			report.setResult(ProtWarriorStat.P_SHIELD_SLAM_CRIT, (double) state.mShieldSlamAction.mNrShieldSlamCrit / shieldSlams * 100);
			report.setResult(ProtWarriorStat.P_SHIELD_SLAM_HIT, (double) state.mShieldSlamAction.mNrShieldSlamHit / shieldSlams * 100);
			report.setResult(ProtWarriorStat.SHIELD_SLAM_AVG_DMG, state.mShieldSlamAction.mShieldSlamDamage / shieldSlams);

			int shieldSlamBonus = state.mModel.mLevelDep.getShieldSlamThreat();
			int nrShieldSlamLand = state.mShieldSlamAction.mNrShieldSlamCrit + state.mShieldSlamAction.mNrShieldSlamHit;
			double shieldSlamThreat = (state.mShieldSlamAction.mShieldSlamDamage + nrShieldSlamLand * shieldSlamBonus) * state.mModel.multThreatStance;
			report.setResult(ProtWarriorStat.SHIELD_SLAM_AVG_THREAT, shieldSlamThreat / shieldSlams);

			report.setResult(ProtWarriorStat.SWORD_AND_BOARD_PROCS, state.mSwordAndBoardEv.mNrSwordAndBoardProcs);
			report.setResult(ProtWarriorStat.SWORD_AND_BOARD_PROC_SHIELD_SLAMS, state.mShieldSlamAction.mNrSwordAndBoardProcShieldSlam);
			report.setResult(ProtWarriorStat.SWORD_AND_BOARD_PROCS_WASTED, state.mSwordAndBoardEv.mNrSwordAndBoardProcs - state.mShieldSlamAction.mNrSwordAndBoardProcShieldSlam);
			report.setResult(ProtWarriorStat.SHIELD_SLAM_TPR, shieldSlamThreat / shieldSlams / state.mModel.shieldSlamCost);
			shieldSlamTPS = shieldSlamThreat / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_SHIELD_SLAM_MISS, 0);
			report.setResult(ProtWarriorStat.P_SHIELD_SLAM_DODGE, 0);
			report.setResult(ProtWarriorStat.P_SHIELD_SLAM_PARRY, 0);
			report.setResult(ProtWarriorStat.P_SHIELD_SLAM_CRIT, 0);
			report.setResult(ProtWarriorStat.P_SHIELD_SLAM_HIT, 0);
			report.setResult(ProtWarriorStat.SHIELD_SLAM_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.SHIELD_SLAM_AVG_THREAT, 0);

			report.setResult(ProtWarriorStat.SWORD_AND_BOARD_PROCS, state.mSwordAndBoardEv.mNrSwordAndBoardProcs);
			report.setResult(ProtWarriorStat.SWORD_AND_BOARD_PROC_SHIELD_SLAMS, 0);
			report.setResult(ProtWarriorStat.SWORD_AND_BOARD_PROCS_WASTED, state.mSwordAndBoardEv.mNrSwordAndBoardProcs);
			report.setResult(ProtWarriorStat.SHIELD_SLAM_TPR, 0);
		}
		report.setResult(ProtWarriorStat.SHIELD_SLAM_TPS, shieldSlamTPS);
		totalTPS += shieldSlamTPS;

		// Devastate
		report.setResult(ProtWarriorStat.DEVASTATE_DPS, primaryTargetData.mDevastateAction.mDevastateDamage / encounterDuration);
		long devastates = primaryTargetData.mDevastateAction.mNrDevastateMiss + primaryTargetData.mDevastateAction.mNrDevastateDodge + primaryTargetData.mDevastateAction.mNrDevastateParry + primaryTargetData.mDevastateAction.mNrDevastateCrit + primaryTargetData.mDevastateAction.mNrDevastateHit;
		report.setResult(ProtWarriorStat.DEVASTATES, devastates);
		double devastateTPS = 0;
		if (devastates > 0) {
			report.setResult(ProtWarriorStat.P_DEVASTATE_MISS, (double) primaryTargetData.mDevastateAction.mNrDevastateMiss / devastates * 100);
			report.setResult(ProtWarriorStat.P_DEVASTATE_DODGE, (double) primaryTargetData.mDevastateAction.mNrDevastateDodge / devastates * 100);
			report.setResult(ProtWarriorStat.P_DEVASTATE_PARRY, (double) primaryTargetData.mDevastateAction.mNrDevastateParry / devastates * 100);
			report.setResult(ProtWarriorStat.P_DEVASTATE_CRIT, (double) primaryTargetData.mDevastateAction.mNrDevastateCrit / devastates * 100);
			report.setResult(ProtWarriorStat.P_DEVASTATE_HIT, (double) primaryTargetData.mDevastateAction.mNrDevastateHit / devastates * 100);
			report.setResult(ProtWarriorStat.DEVASTATE_AVG_DMG, primaryTargetData.mDevastateAction.mDevastateDamage / devastates);
			report.setResult(ProtWarriorStat.DEVASTATE_AVG_THREAT, (primaryTargetData.mDevastateAction.mDevastateDamage * state.mModel.multThreatStance) / devastates);
			report.setResult(ProtWarriorStat.DEVASTATE_TPR, (primaryTargetData.mDevastateAction.mDevastateDamage * state.mModel.multThreatStance) / devastates / state.mModel.devastateCost);

			devastateTPS = (primaryTargetData.mDevastateAction.mDevastateDamage * state.mModel.multThreatStance) / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_DEVASTATE_MISS, 0);
			report.setResult(ProtWarriorStat.P_DEVASTATE_DODGE, 0);
			report.setResult(ProtWarriorStat.P_DEVASTATE_PARRY, 0);
			report.setResult(ProtWarriorStat.P_DEVASTATE_CRIT, 0);
			report.setResult(ProtWarriorStat.P_DEVASTATE_HIT, 0);
			report.setResult(ProtWarriorStat.DEVASTATE_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.DEVASTATE_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.DEVASTATE_TPR, 0);
		}
		report.setResult(ProtWarriorStat.DEVASTATE_TPS, devastateTPS);
		totalTPS += devastateTPS;

		// Revenge
		report.setResult(ProtWarriorStat.REVENGE_DPS, state.mRevengeAction.mRevengeDamage / encounterDuration);
		long revenges = state.mRevengeAction.mNrRevengeMiss + state.mRevengeAction.mNrRevengeDodge + state.mRevengeAction.mNrRevengeParry + state.mRevengeAction.mNrRevengeCrit + state.mRevengeAction.mNrRevengeHit;
		report.setResult(ProtWarriorStat.REVENGES, revenges);
		double revengeTPS = 0;
		if (revenges > 0) {
			report.setResult(ProtWarriorStat.P_REVENGE_MISS, (double) state.mRevengeAction.mNrRevengeMiss / revenges * 100);
			report.setResult(ProtWarriorStat.P_REVENGE_DODGE, (double) state.mRevengeAction.mNrRevengeDodge / revenges * 100);
			report.setResult(ProtWarriorStat.P_REVENGE_PARRY, (double) state.mRevengeAction.mNrRevengeParry / revenges * 100);
			report.setResult(ProtWarriorStat.P_REVENGE_CRIT, (double) state.mRevengeAction.mNrRevengeCrit / revenges * 100);
			report.setResult(ProtWarriorStat.P_REVENGE_HIT, (double) state.mRevengeAction.mNrRevengeHit / revenges * 100);
			report.setResult(ProtWarriorStat.REVENGE_AVG_DMG, state.mRevengeAction.mRevengeDamage / revenges);

			int revengeBonus = state.mModel.mLevelDep.getRevengeThreat();
			int nrRevengeLand = state.mRevengeAction.mNrRevengeCrit + state.mRevengeAction.mNrRevengeHit;
			double revengeThreat = (state.mRevengeAction.mRevengeDamage + nrRevengeLand * revengeBonus) * state.mModel.multThreatStance;
			report.setResult(ProtWarriorStat.REVENGE_AVG_THREAT, revengeThreat / revenges);
			report.setResult(ProtWarriorStat.REVENGE_TPR, revengeThreat / revenges / state.mModel.revengeCost);
			revengeTPS = revengeThreat / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_REVENGE_MISS, 0);
			report.setResult(ProtWarriorStat.P_REVENGE_DODGE, 0);
			report.setResult(ProtWarriorStat.P_REVENGE_PARRY, 0);
			report.setResult(ProtWarriorStat.P_REVENGE_CRIT, 0);
			report.setResult(ProtWarriorStat.P_REVENGE_HIT, 0);
			report.setResult(ProtWarriorStat.REVENGE_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.REVENGE_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.REVENGE_TPR, 0);
		}
		report.setResult(ProtWarriorStat.REVENGE_TPS, revengeTPS);
		totalTPS += revengeTPS;

		// Thunder Clap
		report.setResult(ProtWarriorStat.THUNDER_CLAP_DPS, state.mThunderClapAction.mThunderClapDamage / encounterDuration);
		long thunderClaps = state.mThunderClapAction.mNrThunderClapMiss + state.mThunderClapAction.mNrThunderClapCrit + state.mThunderClapAction.mNrThunderClapHit;
		report.setResult(ProtWarriorStat.THUNDER_CLAPS, thunderClaps);
		double thunderClapTPS = 0;
		if (thunderClaps > 0) {
			report.setResult(ProtWarriorStat.P_THUNDER_CLAP_MISS, (double) state.mThunderClapAction.mNrThunderClapMiss / thunderClaps * 100);
			report.setResult(ProtWarriorStat.P_THUNDER_CLAP_CRIT, (double) state.mThunderClapAction.mNrThunderClapCrit / thunderClaps * 100);
			report.setResult(ProtWarriorStat.P_THUNDER_CLAP_HIT, (double) state.mThunderClapAction.mNrThunderClapHit / thunderClaps * 100);
			report.setResult(ProtWarriorStat.THUNDER_CLAP_AVG_DMG, state.mThunderClapAction.mThunderClapDamage / thunderClaps);
			report.setResult(ProtWarriorStat.THUNDER_CLAP_AVG_THREAT, (state.mThunderClapAction.mThunderClapDamage * state.mModel.multThreatStance) / thunderClaps);
			report.setResult(ProtWarriorStat.THUNDER_CLAP_TPR, (state.mThunderClapAction.mThunderClapDamage * state.mModel.multThreatStance) / thunderClaps / state.mModel.thunderClapCost);
			report.setResult(ProtWarriorStat.THUNDER_CLAP_UPTIME, state.mThunderClapAction.getUpTime() / encounterDuration * 100);

			thunderClapTPS = (state.mThunderClapAction.mThunderClapDamage * state.mModel.multThreatStance) / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_THUNDER_CLAP_MISS, 0);
			report.setResult(ProtWarriorStat.P_THUNDER_CLAP_CRIT, 0);
			report.setResult(ProtWarriorStat.P_THUNDER_CLAP_HIT, 0);
			report.setResult(ProtWarriorStat.THUNDER_CLAP_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.THUNDER_CLAP_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.THUNDER_CLAP_TPR, 0);
			report.setResult(ProtWarriorStat.THUNDER_CLAP_UPTIME, 0);
		}
		report.setResult(ProtWarriorStat.THUNDER_CLAP_TPS, thunderClapTPS);
		totalTPS += thunderClapTPS;

		// Rend
		report.setResult(ProtWarriorStat.REND_DPS, (state.mRendAction.mRendDamage + state.mRendAction.mRendDoTDamage) / encounterDuration);
		report.setResult(ProtWarriorStat.REND_DIRECT_DPS, state.mRendAction.mRendDamage / encounterDuration);
		report.setResult(ProtWarriorStat.REND_DOT_DPS, state.mRendAction.mRendDoTDamage / encounterDuration);
		long rends = state.mRendAction.mNrRendMiss + state.mRendAction.mNrRendDodge + state.mRendAction.mNrRendParry + state.mRendAction.mNrRendCrit + state.mRendAction.mNrRendHit;
		report.setResult(ProtWarriorStat.RENDS, rends);
		double rendTPS = 0;
		if (rends > 0) {
			report.setResult(ProtWarriorStat.P_REND_MISS, (double) state.mRendAction.mNrRendMiss / rends * 100);
			report.setResult(ProtWarriorStat.P_REND_DODGE, (double) state.mRendAction.mNrRendDodge / rends * 100);
			report.setResult(ProtWarriorStat.P_REND_PARRY, (double) state.mRendAction.mNrRendParry / rends * 100);
			report.setResult(ProtWarriorStat.P_REND_CRIT, (double) state.mRendAction.mNrRendCrit / rends * 100);
			report.setResult(ProtWarriorStat.P_REND_HIT, (double) state.mRendAction.mNrRendHit / rends * 100);
			report.setResult(ProtWarriorStat.REND_AVG_DMG, state.mRendAction.mRendDamage / rends);
			double rendAvgThreat = state.mRendAction.mRendDamage * state.mModel.multThreatStance / rends;
			report.setResult(ProtWarriorStat.REND_AVG_THREAT, rendAvgThreat);
			long rendTicks = state.mRendAction.mNrRendDoTCrit + state.mRendAction.mNrRendDoTHit;
			report.setResult(ProtWarriorStat.REND_TICKS, rendTicks);
			if (rendTicks > 0) {
				report.setResult(ProtWarriorStat.P_REND_DOT_CRIT, (double) state.mRendAction.mNrRendDoTCrit / rendTicks * 100);
				report.setResult(ProtWarriorStat.P_REND_DOT_HIT, (double) state.mRendAction.mNrRendDoTHit / rendTicks * 100);
				report.setResult(ProtWarriorStat.REND_DOT_AVG_DMG, state.mRendAction.mRendDoTDamage / rendTicks);
				report.setResult(ProtWarriorStat.REND_DOT_AVG_THREAT, state.mRendAction.mRendDoTDamage * state.mModel.multThreatStance / rendTicks);
			} else {
				// This should be extremely unlikely.
				report.setResult(ProtWarriorStat.P_REND_DOT_CRIT, 0);
				report.setResult(ProtWarriorStat.P_REND_DOT_HIT, 0);
				report.setResult(ProtWarriorStat.REND_DOT_AVG_DMG, 0);
				report.setResult(ProtWarriorStat.REND_DOT_AVG_THREAT, 0);
			}
			report.setResult(ProtWarriorStat.REND_UPTIME, state.mRendAction.getUpTime() / encounterDuration * 100);
			report.setResult(ProtWarriorStat.REND_TPR, rendAvgThreat / state.mModel.rendCost);
			report.setResult(ProtWarriorStat.REND_DOT_TPS, (state.mRendAction.mRendDoTDamage * state.mModel.multThreatStance) / encounterDuration);
			report.setResult(ProtWarriorStat.REND_DIRECT_TPS, (state.mRendAction.mRendDamage * state.mModel.multThreatStance) / encounterDuration);
			rendTPS = ((state.mRendAction.mRendDamage + state.mRendAction.mRendDoTDamage) * state.mModel.multThreatStance) / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_REND_MISS, 0);
			report.setResult(ProtWarriorStat.P_REND_DODGE, 0);
			report.setResult(ProtWarriorStat.P_REND_PARRY, 0);
			report.setResult(ProtWarriorStat.P_REND_CRIT, 0);
			report.setResult(ProtWarriorStat.P_REND_HIT, 0);
			report.setResult(ProtWarriorStat.REND_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.REND_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.REND_TICKS, 0);
			report.setResult(ProtWarriorStat.P_REND_DOT_CRIT, 0);
			report.setResult(ProtWarriorStat.P_REND_DOT_HIT, 0);
			report.setResult(ProtWarriorStat.REND_DOT_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.REND_DOT_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.REND_UPTIME, 0);
			report.setResult(ProtWarriorStat.REND_TPR, 0);
			report.setResult(ProtWarriorStat.REND_DOT_TPS, 0);
			report.setResult(ProtWarriorStat.REND_DIRECT_TPS, 0);
		}
		report.setResult(ProtWarriorStat.REND_TPS, rendTPS);
		totalTPS += rendTPS;

		// Concussion Blow
		report.setResult(ProtWarriorStat.CONCUSSION_BLOW_DPS, state.mConcussionBlowAction.mConcussionBlowDamage / encounterDuration);
		long concussionBlows = state.mConcussionBlowAction.mNrConcussionBlowMiss + state.mConcussionBlowAction.mNrConcussionBlowDodge + state.mConcussionBlowAction.mNrConcussionBlowParry + state.mConcussionBlowAction.mNrConcussionBlowCrit + state.mConcussionBlowAction.mNrConcussionBlowHit;
		report.setResult(ProtWarriorStat.CONCUSSION_BLOWS, concussionBlows);
		double concussionBlowTPS = 0;
		if (concussionBlows > 0) {
			report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_MISS, (double) state.mConcussionBlowAction.mNrConcussionBlowMiss / concussionBlows * 100);
			report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_DODGE, (double) state.mConcussionBlowAction.mNrConcussionBlowDodge / concussionBlows * 100);
			report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_PARRY, (double) state.mConcussionBlowAction.mNrConcussionBlowParry / concussionBlows * 100);
			report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_CRIT, (double) state.mConcussionBlowAction.mNrConcussionBlowCrit / concussionBlows * 100);
			report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_HIT, (double) state.mConcussionBlowAction.mNrConcussionBlowHit / concussionBlows * 100);
			report.setResult(ProtWarriorStat.CONCUSSION_BLOW_AVG_DMG, state.mConcussionBlowAction.mConcussionBlowDamage / concussionBlows);
			report.setResult(ProtWarriorStat.CONCUSSION_BLOW_AVG_THREAT, (state.mConcussionBlowAction.mConcussionBlowDamage * state.mModel.mLevelDep.getConcussionBlowThreatMut() * state.mModel.multThreatStance) / concussionBlows);
			report.setResult(ProtWarriorStat.CONCUSSION_BLOW_TPR, (state.mConcussionBlowAction.mConcussionBlowDamage * state.mModel.mLevelDep.getConcussionBlowThreatMut() * state.mModel.multThreatStance) / concussionBlows / state.mModel.concussionBlowCost);

			concussionBlowTPS = (state.mConcussionBlowAction.mConcussionBlowDamage * state.mModel.mLevelDep.getConcussionBlowThreatMut() * state.mModel.multThreatStance) / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_MISS, 0);
			report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_DODGE, 0);
			report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_PARRY, 0);
			report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_CRIT, 0);
			report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_HIT, 0);
			report.setResult(ProtWarriorStat.CONCUSSION_BLOW_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.CONCUSSION_BLOW_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.CONCUSSION_BLOW_TPR, 0);
		}
		report.setResult(ProtWarriorStat.CONCUSSION_BLOW_TPS, concussionBlowTPS);
		totalTPS += concussionBlowTPS;

		// Shockwave
		report.setResult(ProtWarriorStat.SHOCKWAVE_DPS, state.mShockwaveAction.mShockwaveDamage / encounterDuration);
		long shockwaves = state.mShockwaveAction.mNrShockwaveMiss + state.mShockwaveAction.mNrShockwaveCrit + state.mShockwaveAction.mNrShockwaveHit;
		report.setResult(ProtWarriorStat.SHOCKWAVES, shockwaves);
		double shockwaveTPS = 0;
		if (shockwaves > 0) {
			report.setResult(ProtWarriorStat.P_SHOCKWAVE_MISS, (double) state.mShockwaveAction.mNrShockwaveMiss / shockwaves * 100);
			report.setResult(ProtWarriorStat.P_SHOCKWAVE_CRIT, (double) state.mShockwaveAction.mNrShockwaveCrit / shockwaves * 100);
			report.setResult(ProtWarriorStat.P_SHOCKWAVE_HIT, (double) state.mShockwaveAction.mNrShockwaveHit / shockwaves * 100);
			report.setResult(ProtWarriorStat.SHOCKWAVE_AVG_DMG, state.mShockwaveAction.mShockwaveDamage / shockwaves);
			report.setResult(ProtWarriorStat.SHOCKWAVE_AVG_THREAT, (state.mShockwaveAction.mShockwaveDamage * state.mModel.multThreatStance) / shockwaves);
			report.setResult(ProtWarriorStat.SHOCKWAVE_TPR, (state.mShockwaveAction.mShockwaveDamage * state.mModel.multThreatStance) / shockwaves / state.mModel.shockwaveCost);

			shockwaveTPS = (state.mShockwaveAction.mShockwaveDamage * state.mModel.multThreatStance) / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_SHOCKWAVE_MISS, 0);
			report.setResult(ProtWarriorStat.P_SHOCKWAVE_CRIT, 0);
			report.setResult(ProtWarriorStat.P_SHOCKWAVE_HIT, 0);
			report.setResult(ProtWarriorStat.SHOCKWAVE_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.SHOCKWAVE_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.SHOCKWAVE_TPR, 0);
		}
		report.setResult(ProtWarriorStat.SHOCKWAVE_TPS, shockwaveTPS);
		totalTPS += shockwaveTPS;

		// Heroic Throw
		report.setResult(ProtWarriorStat.HEROIC_THROW_DPS, state.mHeroicThrowAction.mHeroicThrowDamage / encounterDuration);
		long heroicThrows = state.mHeroicThrowAction.mNrHeroicThrowMiss + state.mHeroicThrowAction.mNrHeroicThrowCrit + state.mHeroicThrowAction.mNrHeroicThrowHit;
		report.setResult(ProtWarriorStat.HEROIC_THROWS, heroicThrows);
		double heroicThrowTPS = 0;
		if (heroicThrows > 0) {
			report.setResult(ProtWarriorStat.P_HEROIC_THROW_MISS, (double) state.mHeroicThrowAction.mNrHeroicThrowMiss / heroicThrows * 100);
			report.setResult(ProtWarriorStat.P_HEROIC_THROW_CRIT, (double) state.mHeroicThrowAction.mNrHeroicThrowCrit / heroicThrows * 100);
			report.setResult(ProtWarriorStat.P_HEROIC_THROW_HIT, (double) state.mHeroicThrowAction.mNrHeroicThrowHit / heroicThrows * 100);
			report.setResult(ProtWarriorStat.HEROIC_THROW_AVG_DMG, state.mHeroicThrowAction.mHeroicThrowDamage / heroicThrows);
			report.setResult(ProtWarriorStat.HEROIC_THROW_AVG_THREAT, (state.mHeroicThrowAction.mHeroicThrowDamage * state.mModel.mLevelDep.getHeroicThrowThreatMult() * state.mModel.multThreatStance) / heroicThrows);

			heroicThrowTPS = (state.mHeroicThrowAction.mHeroicThrowDamage * state.mModel.mLevelDep.getHeroicThrowThreatMult() * state.mModel.multThreatStance) / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_HEROIC_THROW_MISS, 0);
			report.setResult(ProtWarriorStat.P_HEROIC_THROW_CRIT, 0);
			report.setResult(ProtWarriorStat.P_HEROIC_THROW_HIT, 0);
			report.setResult(ProtWarriorStat.HEROIC_THROW_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.HEROIC_THROW_AVG_THREAT, 0);
		}
		report.setResult(ProtWarriorStat.HEROIC_THROW_TPS, heroicThrowTPS);
		totalTPS += heroicThrowTPS;

		// Victory Rush
		double victoryRushHealTPS = state.mVictoryRushAction.mVictoryRushHeal * 0.5 * state.mModel.multThreatStance / encounterDuration;
		report.setResult(ProtWarriorStat.VICTORY_RUSH_DPS, state.mVictoryRushAction.mVictoryRushDamage / encounterDuration);
		long victoryRushes = state.mVictoryRushAction.mNrVictoryRushMiss + state.mVictoryRushAction.mNrVictoryRushDodge + state.mVictoryRushAction.mNrVictoryRushParry + state.mVictoryRushAction.mNrVictoryRushCrit + state.mVictoryRushAction.mNrVictoryRushHit;
		report.setResult(ProtWarriorStat.VICTORY_RUSHS, victoryRushes);
		double victoryRushTPS = 0;
		if (victoryRushes > 0) {
			report.setResult(ProtWarriorStat.P_VICTORY_RUSH_MISS, (double) state.mVictoryRushAction.mNrVictoryRushMiss / victoryRushes * 100);
			report.setResult(ProtWarriorStat.P_VICTORY_RUSH_DODGE, (double) state.mVictoryRushAction.mNrVictoryRushDodge / victoryRushes * 100);
			report.setResult(ProtWarriorStat.P_VICTORY_RUSH_PARRY, (double) state.mVictoryRushAction.mNrVictoryRushParry / victoryRushes * 100);
			report.setResult(ProtWarriorStat.P_VICTORY_RUSH_CRIT, (double) state.mVictoryRushAction.mNrVictoryRushCrit / victoryRushes * 100);
			report.setResult(ProtWarriorStat.P_VICTORY_RUSH_HIT, (double) state.mVictoryRushAction.mNrVictoryRushHit / victoryRushes * 100);
			report.setResult(ProtWarriorStat.VICTORY_RUSH_AVG_DMG, state.mVictoryRushAction.mVictoryRushDamage / victoryRushes);
			report.setResult(ProtWarriorStat.VICTORY_RUSH_AVG_HEAL, state.mVictoryRushAction.mVictoryRushHeal / victoryRushes);
			report.setResult(ProtWarriorStat.VICTORY_RUSH_AVG_THREAT, (state.mVictoryRushAction.mVictoryRushDamage * state.mModel.multThreatStance) / victoryRushes);

			report.setResult(ProtWarriorStat.IMPENDING_VICTORY_PROCS, state.mImpendingVictoryEv.mNrProcs);
			report.setResult(ProtWarriorStat.IMPENDING_VICTORY_PROCS_WASTED, state.mImpendingVictoryEv.mNrProcs - victoryRushes);

			victoryRushTPS = (state.mVictoryRushAction.mVictoryRushDamage * state.mModel.multThreatStance) / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_VICTORY_RUSH_MISS, 0);
			report.setResult(ProtWarriorStat.P_VICTORY_RUSH_DODGE, 0);
			report.setResult(ProtWarriorStat.P_VICTORY_RUSH_PARRY, 0);
			report.setResult(ProtWarriorStat.P_VICTORY_RUSH_CRIT, 0);
			report.setResult(ProtWarriorStat.P_VICTORY_RUSH_HIT, 0);
			report.setResult(ProtWarriorStat.VICTORY_RUSH_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.VICTORY_RUSH_AVG_HEAL, 0);
			report.setResult(ProtWarriorStat.VICTORY_RUSH_AVG_THREAT, 0);
		}
		report.setResult(ProtWarriorStat.VICTORY_RUSH_TPS, victoryRushTPS + victoryRushHealTPS);
		totalTPS += victoryRushTPS;

		// Heroic Strike
		report.setResult(ProtWarriorStat.HEROIC_STRIKE_DPS, state.mHeroicStrikeAction.mHeroicStrikeDamage / encounterDuration);
		long heroicStrikes = state.mHeroicStrikeAction.mNrHeroicStrikeMiss + state.mHeroicStrikeAction.mNrHeroicStrikeDodge + state.mHeroicStrikeAction.mNrHeroicStrikeParry + state.mHeroicStrikeAction.mNrHeroicStrikeCrit + state.mHeroicStrikeAction.mNrHeroicStrikeHit;
		report.setResult(ProtWarriorStat.HEROIC_STRIKES, heroicStrikes);
		double heroicStrikeTPS = 0;
		if (heroicStrikes > 0) {
			report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_MISS, (double) state.mHeroicStrikeAction.mNrHeroicStrikeMiss / heroicStrikes * 100);
			report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_DODGE, (double) state.mHeroicStrikeAction.mNrHeroicStrikeDodge / heroicStrikes * 100);
			report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_PARRY, (double) state.mHeroicStrikeAction.mNrHeroicStrikeParry / heroicStrikes * 100);
			report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_CRIT, (double) state.mHeroicStrikeAction.mNrHeroicStrikeCrit / heroicStrikes * 100);
			report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_HIT, (double) state.mHeroicStrikeAction.mNrHeroicStrikeHit / heroicStrikes * 100);
			report.setResult(ProtWarriorStat.HEROIC_STRIKE_AVG_DMG, state.mHeroicStrikeAction.mHeroicStrikeDamage / heroicStrikes);

			int heroicStrikeBonus = state.mModel.mLevelDep.getHeroicStrikeThreat();
			int nrHeroicStrikeLand = state.mHeroicStrikeAction.mNrHeroicStrikeCrit + state.mHeroicStrikeAction.mNrHeroicStrikeHit;
			double heroicStrikeThreat = (state.mHeroicStrikeAction.mHeroicStrikeDamage + nrHeroicStrikeLand * heroicStrikeBonus) * state.mModel.multThreatStance;
			report.setResult(ProtWarriorStat.HEROIC_STRIKE_AVG_THREAT, heroicStrikeThreat / heroicStrikes);
			report.setResult(ProtWarriorStat.HEROIC_STRIKE_TPR, heroicStrikeThreat / heroicStrikes / state.mModel.heroicStrikeCost);
			heroicStrikeTPS = heroicStrikeThreat / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_MISS, 0);
			report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_DODGE, 0);
			report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_PARRY, 0);
			report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_CRIT, 0);
			report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_HIT, 0);
			report.setResult(ProtWarriorStat.HEROIC_STRIKE_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.HEROIC_STRIKE_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.HEROIC_STRIKE_TPR, 0);
		}
		report.setResult(ProtWarriorStat.HEROIC_STRIKE_TPS, heroicStrikeTPS);
		totalTPS += heroicStrikeTPS;

		// Cleave
		report.setResult(ProtWarriorStat.CLEAVE_DPS, state.mCleaveAction.mCleaveDamage / encounterDuration);
		long cleaves = state.mCleaveAction.mNrCleaveMiss + state.mCleaveAction.mNrCleaveDodge + state.mCleaveAction.mNrCleaveParry + state.mCleaveAction.mNrCleaveCrit + state.mCleaveAction.mNrCleaveHit;
		report.setResult(ProtWarriorStat.CLEAVES, cleaves);
		double cleaveTPS = 0;
		if (cleaves > 0) {
			report.setResult(ProtWarriorStat.P_CLEAVE_MISS, (double) state.mCleaveAction.mNrCleaveMiss / cleaves * 100);
			report.setResult(ProtWarriorStat.P_CLEAVE_DODGE, (double) state.mCleaveAction.mNrCleaveDodge / cleaves * 100);
			report.setResult(ProtWarriorStat.P_CLEAVE_PARRY, (double) state.mCleaveAction.mNrCleaveParry / cleaves * 100);
			report.setResult(ProtWarriorStat.P_CLEAVE_CRIT, (double) state.mCleaveAction.mNrCleaveCrit / cleaves * 100);
			report.setResult(ProtWarriorStat.P_CLEAVE_HIT, (double) state.mCleaveAction.mNrCleaveHit / cleaves * 100);
			report.setResult(ProtWarriorStat.CLEAVE_AVG_DMG, state.mCleaveAction.mCleaveDamage / cleaves);

			int cleaveBonus = state.mModel.mLevelDep.getCleaveThreat();
			int nrCleaveLand = state.mCleaveAction.mNrCleaveCrit + state.mCleaveAction.mNrCleaveHit;
			double cleaveThreat = (state.mCleaveAction.mCleaveDamage + nrCleaveLand * cleaveBonus) * state.mModel.multThreatStance;
			report.setResult(ProtWarriorStat.CLEAVE_AVG_THREAT, cleaveThreat / cleaves);
			report.setResult(ProtWarriorStat.CLEAVE_TPR, cleaveThreat / cleaves / state.mModel.cleaveCost);
			cleaveTPS = cleaveThreat / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_CLEAVE_MISS, 0);
			report.setResult(ProtWarriorStat.P_CLEAVE_DODGE, 0);
			report.setResult(ProtWarriorStat.P_CLEAVE_PARRY, 0);
			report.setResult(ProtWarriorStat.P_CLEAVE_CRIT, 0);
			report.setResult(ProtWarriorStat.P_CLEAVE_HIT, 0);
			report.setResult(ProtWarriorStat.CLEAVE_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.CLEAVE_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.CLEAVE_TPR, 0);
		}
		report.setResult(ProtWarriorStat.CLEAVE_TPS, cleaveTPS);
		totalTPS += cleaveTPS;

		// Retaliation
		report.setResult(ProtWarriorStat.RETALIATION_DPS, state.mRetaliationAction.mRetaliationDamage / encounterDuration);
		long retaliations = state.mRetaliationAction.mNrRetaliationMiss + state.mRetaliationAction.mNrRetaliationDodge + state.mRetaliationAction.mNrRetaliationParry + state.mRetaliationAction.mNrRetaliationCrit + state.mRetaliationAction.mNrRetaliationHit;
		report.setResult(ProtWarriorStat.RETALIATIONS, retaliations);
		double retaliationTPS = 0;
		if (retaliations > 0) {
			report.setResult(ProtWarriorStat.P_RETALIATION_MISS, (double) state.mRetaliationAction.mNrRetaliationMiss / retaliations * 100);
			report.setResult(ProtWarriorStat.P_RETALIATION_DODGE, (double) state.mRetaliationAction.mNrRetaliationDodge / retaliations * 100);
			report.setResult(ProtWarriorStat.P_RETALIATION_PARRY, (double) state.mRetaliationAction.mNrRetaliationParry / retaliations * 100);
			report.setResult(ProtWarriorStat.P_RETALIATION_CRIT, (double) state.mRetaliationAction.mNrRetaliationCrit / retaliations * 100);
			report.setResult(ProtWarriorStat.P_RETALIATION_HIT, (double) state.mRetaliationAction.mNrRetaliationHit / retaliations * 100);
			report.setResult(ProtWarriorStat.RETALIATION_AVG_DMG, state.mRetaliationAction.mRetaliationDamage / retaliations);
			double retaliationThreat = state.mRetaliationAction.mRetaliationDamage * state.mModel.multThreatStance;
			report.setResult(ProtWarriorStat.RETALIATION_AVG_THREAT, retaliationThreat / retaliations);
			retaliationTPS = retaliationThreat / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.P_RETALIATION_MISS, 0);
			report.setResult(ProtWarriorStat.P_RETALIATION_DODGE, 0);
			report.setResult(ProtWarriorStat.P_RETALIATION_PARRY, 0);
			report.setResult(ProtWarriorStat.P_RETALIATION_CRIT, 0);
			report.setResult(ProtWarriorStat.P_RETALIATION_HIT, 0);
			report.setResult(ProtWarriorStat.RETALIATION_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.RETALIATION_AVG_THREAT, 0);
		}
		report.setResult(ProtWarriorStat.RETALIATION_TPS, retaliationTPS);
		totalTPS += retaliationTPS;

		// Combust (Tier 12 2pc)
		report.setResult(ProtWarriorStat.COMBUST_DPS, (double) state.mTier_12_2pcEv.mCombustDamage / encounterDuration);
		int nrCombustTicks = state.mTier_12_2pcEv.mNrCombustTicks;
		report.setResult(ProtWarriorStat.COMBUST_TICKS, nrCombustTicks);
		double combustTPS = 0;
		if (nrCombustTicks > 0) {
			report.setResult(ProtWarriorStat.COMBUST_AVG_DMG, (double) state.mTier_12_2pcEv.mCombustDamage / nrCombustTicks);
			report.setResult(ProtWarriorStat.COMBUST_ROLLED, state.mTier_12_2pcEv.mNrCombustRolled);
			report.setResult(ProtWarriorStat.COMBUST_ROLLED_DMG, state.mTier_12_2pcEv.mCombustDmgRolled);
			report.setResult(ProtWarriorStat.COMBUST_MUNCHED, state.mTier_12_2pcEv.mNrCombustMunch);
			report.setResult(ProtWarriorStat.COMBUST_MUNCHED_DMG, state.mTier_12_2pcEv.mCombustDmgLostMunch);
			report.setResult(ProtWarriorStat.COMBUST_REFRESH_LOST, state.mTier_12_2pcEv.mNrCombustRefreshLost);
			report.setResult(ProtWarriorStat.COMBUST_REFRESH_LOST_DMG, state.mTier_12_2pcEv.mCombustDmgLostRefresh);
			double combustAvgThreat = (state.mTier_12_2pcEv.mCombustDamage * state.mModel.multThreatStance) / nrCombustTicks;
			report.setResult(ProtWarriorStat.COMBUST_AVG_THREAT, combustAvgThreat);
			combustTPS = (state.mTier_12_2pcEv.mCombustDamage * state.mModel.multThreatStance) / encounterDuration;
		} else {
			report.setResult(ProtWarriorStat.COMBUST_AVG_DMG, 0);
			report.setResult(ProtWarriorStat.COMBUST_AVG_THREAT, 0);
			report.setResult(ProtWarriorStat.COMBUST_ROLLED, 0);
			report.setResult(ProtWarriorStat.COMBUST_ROLLED_DMG, 0);
			report.setResult(ProtWarriorStat.COMBUST_MUNCHED, 0);
			report.setResult(ProtWarriorStat.COMBUST_MUNCHED_DMG, 0);
			report.setResult(ProtWarriorStat.COMBUST_REFRESH_LOST, 0);
			report.setResult(ProtWarriorStat.COMBUST_REFRESH_LOST_DMG, 0);
		}
		report.setResult(ProtWarriorStat.COMBUST_TPS, combustTPS);
		totalTPS += combustTPS;

		// Other sources
		double otherTPS = (state.mProcPhysDamage + state.mProcSpellDamage) * state.mModel.multThreatStance / encounterDuration + demoralizingShoutTPS + bastionOfDefenseTPS + rudeInterruptionTPS + shoutTPS + enragedRegenerationTPS + bloodCrazeTPS;
		otherTPS += state.mProcHeal * 0.5 * state.mModel.multThreatStance / encounterDuration;
		report.setResult(ProtWarriorStat.OTHER_DPS, (state.mProcPhysDamage + state.mProcSpellDamage) / encounterDuration);
		report.setResult(ProtWarriorStat.OTHER_HPS, (state.mProcHeal) / encounterDuration);
		report.setResult(ProtWarriorStat.OTHER_TPS, otherTPS);
		totalTPS += otherTPS;

		report.setResult(ProtWarriorStat.TOTAL_DTPS, mobWhiteAttackDTPS + mobYellowAttackDTPS + mobMagicAttackDTPS);
		if (state.mMobWhiteBaseDamage + state.mMobYellowBaseDamage > 0)
			report.setResult(ProtWarriorStat.PHYSICAL_DAMAGE_REDUCTION, (1.0f - (double) (state.mMobWhiteDamageTaken + state.mMobYellowDamageTaken) / (state.mMobWhiteBaseDamage + state.mMobYellowBaseDamage)) * 100);
		if (state.mMobMagicBaseDamage > 0)
			report.setResult(ProtWarriorStat.MAGIC_DAMAGE_REDUCTION, (1.0f - (double) state.mMobMagicDamageTaken / state.mMobMagicBaseDamage) * 100);

		// Derive TPS breakdowns.
		report.setResult(ProtWarriorStat.P_RAGE_TPS, rageGainTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_WHITE_ATTACK_TPS, whiteAttackTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_DEEP_WOUNDS_TPS, deepWoundsTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_SHIELD_SLAM_TPS, shieldSlamTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_DEVASTATE_TPS, devastateTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_REVENGE_TPS, revengeTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_THUNDER_CLAP_TPS, thunderClapTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_REND_TPS, rendTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_CONCUSSION_BLOW_TPS, concussionBlowTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_SHOCKWAVE_TPS, shockwaveTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_VICTORY_RUSH_TPS, victoryRushTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_HEROIC_THROW_TPS, heroicThrowTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_HEROIC_STRIKE_TPS, heroicStrikeTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_CLEAVE_TPS, cleaveTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_RETALIATION_TPS, retaliationTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_COMBUST_TPS, combustTPS / totalTPS * 100);
		report.setResult(ProtWarriorStat.P_OTHER_TPS, otherTPS / totalTPS * 100);

		// Work out total damage for debugging/DPS calculations.
		long whiteDmg = state.mMeleeEv.mWhiteDamage;
		long deepWoundsDmg = state.mDeepWoundsEv.mDeepWoundsDamage;
		long shieldSlamDmg = state.mShieldSlamAction.mShieldSlamDamage;
		long devastateDmg = primaryTargetData.mDevastateAction.mDevastateDamage;
		long revengeDmg = state.mRevengeAction.mRevengeDamage;
		long thunderClapDmg = state.mThunderClapAction.mThunderClapDamage;
		long rendDmg = state.mRendAction.mRendDamage + state.mRendAction.mRendDoTDamage;
		long concussionBlowDmg = state.mConcussionBlowAction.mConcussionBlowDamage;
		long shockwaveDmg = state.mShockwaveAction.mShockwaveDamage;
		long heroicThrowDmg = state.mHeroicThrowAction.mHeroicThrowDamage;
		long heroicStrikeDmg = state.mHeroicStrikeAction.mHeroicStrikeDamage;
		long victoryRushDmg = state.mVictoryRushAction.mVictoryRushDamage;
		long cleaveDmg = state.mCleaveAction.mCleaveDamage;
		long retaliationDmg = state.mRetaliationAction.mRetaliationDamage;
		long combustDmg = state.mTier_12_2pcEv.mCombustDamage;
		long otherDmg = state.mProcPhysDamage + state.mProcSpellDamage;
		long totalDamage = whiteDmg + deepWoundsDmg + shieldSlamDmg + devastateDmg + revengeDmg + thunderClapDmg + rendDmg + concussionBlowDmg + shockwaveDmg + heroicThrowDmg + victoryRushDmg + heroicStrikeDmg + cleaveDmg + retaliationDmg + combustDmg + otherDmg;

		double dps = totalDamage / encounterDuration;
		double totalDamageTaken = state.mMobWhiteDamageTaken + state.mMobYellowDamageTaken + state.mMobMagicDamageTaken;

		report.setResult(ProtWarriorStat.TOTAL_DAMAGE, totalDamage);
		report.setResult(ProtWarriorStat.TOTAL_DAMAGE_TAKEN, totalDamageTaken);

		report.setResult(Stat.THREAT_PER_SECOND, totalTPS);
		report.setResult(Stat.DAMAGE_DEALT_PER_SECOND, dps);

		// Calculate total mitigation to allow for reporting stat values around that in the future.
		// Note: This requires *a lot* of iterations to gain valid results.
		double mitigationValue = 0;
		if (state.mMobWhiteBaseDamage + state.mMobYellowBaseDamage + state.mMobMagicBaseDamage > 0) {
			double totalBaseDamage = state.mMobWhiteBaseDamage + state.mMobYellowBaseDamage + state.mMobMagicBaseDamage;
			mitigationValue = 1 / (1 - (1 - totalDamageTaken / totalBaseDamage));
			report.setResult(ProtWarriorStat.TOTAL_DAMAGE_BASE, totalBaseDamage);
		}
		report.setResult(Stat.MITIGATION_VALUE, mitigationValue);

		report.mShouldReportMitigation = state.mSimEngine.getConfig().getBool(SimulatorProperty.REPORTMIT);

		// Procs
		report.mProcUptimes = new HashMap<ProcEffect, Double>();
		for (SimulationProc proc : state.mSimEngine.getProcs()) {
			ProcEffect procEffect = proc.getProcEffect();
			if (procEffect.buffDuration == 0)
				report.mProcUptimes.put(proc.getProcEffect(), (double) proc.mNrProcs);
			else
				report.mProcUptimes.put(proc.getProcEffect(), proc.getUpTime() / state.mSimEngine.getEndTime());
		}

		if (report.mDumpVengeance || report.mDumpWhiteDamage || report.mDumpYellowDamage || report.mDumpMagicDamage) {
			report.mConfig = state.mSimEngine.getConfig().toString();
			if (report.mDumpVengeance) report.mVengeanceSamples = state.mVengeanceEv.mVengeanceSamples;
			if (report.mDumpWhiteDamage && state.mMobWhiteDamageTakenSamples != null) {
				int i = 0;
				report.mWhiteDamageSamples = new double[state.mMobWhiteDamageTakenSamples.size()];
				for (Double val : state.mMobWhiteDamageTakenSamples) report.mWhiteDamageSamples[i++] = val;
			}
			if (report.mDumpYellowDamage && state.mMobYellowDamageTakenSamples != null) {
				int i = 0;
				report.mYellowDamageSamples = new double[state.mMobYellowDamageTakenSamples.size()];
				for (Double val : state.mMobYellowDamageTakenSamples) report.mYellowDamageSamples[i++] = val;
			}
			if (report.mDumpMagicDamage && state.mMobMagicDamageTakenSamples != null) {
				int i = 0;
				report.mMagicDamageSamples = new double[state.mMobMagicDamageTakenSamples.size()];
				for (Double val : state.mMobMagicDamageTakenSamples) report.mMagicDamageSamples[i++] = val;
			}
		}

		return report;
	}

	@Override
	public String getVersionInfo() {
		return VERSION_INFO;
	}

	@Override
	public Class<?>[] getExportClasses() {
		return new Class<?>[] {
				Action.class,
				ProtWarriorSimulator.class
		};
	}

	@Override
	public boolean canPerformAction(IStatus state, IAction action) {
		// The prot warrior model can always perform all actions. (GCD checking done elsewhere.)
		return true;
	}

	@Override
	public boolean shouldQueryStrategy(IStatus state) {
		// The prot warrior model is always on target, so there's never any time to not query the strategy.
		return true;
	}

	@Override
	public SimulationTarget getActiveTarget(IStatus state) {
		return ((ProtWarriorSimulator) state).mActiveTarget;
	}
}
